/*******************************************************************************
 * Copyright 1995-2018 Hitachi Vantara Corporation. All rights reserved.
 * Copyright 2019 Dell Inc.
 *
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 *
 *******************************************************************************/

// coredata provides clients used for integration with the core-data service.
package coredata

import (
	"context"
	"encoding/json"
	"net/url"
	"strconv"

	"github.com/edgexfoundry/go-mod-core-contracts/clients"
	"github.com/edgexfoundry/go-mod-core-contracts/clients/interfaces"
	"github.com/edgexfoundry/go-mod-core-contracts/models"
)

// EventClient defines the interface for interactions with the Event endpoint on the EdgeX Foundry core-data service.
type EventClient interface {
	// Events gets a list of all events
	Events(ctx context.Context) ([]models.Event, error)
	// Event gets an event by its id
	Event(ctx context.Context, id string) (models.Event, error)
	// EventCount returns the total count of events
	EventCount(ctx context.Context) (int, error)
	// EventCountForDevice returns the total event count for the specified device
	EventCountForDevice(ctx context.Context, deviceId string) (int, error)
	// EventsForDevice returns events up to a specified number that were generated by a given device
	EventsForDevice(ctx context.Context, deviceID string, limit int) ([]models.Event, error)
	// EventsForInterval returns events generated within a specific time period
	EventsForInterval(ctx context.Context, start int, end int, limit int) ([]models.Event, error)
	// EventsForDeviceAndValueDescriptor returns events for the specified device and value descriptor
	EventsForDeviceAndValueDescriptor(ctx context.Context, deviceId string, vd string, limit int) ([]models.Event, error)
	// Add will post a new event
	Add(ctx context.Context, event *models.Event) (string, error)
	// AddBytes posts a new event using an array of bytes, supporting encoding of the event by the caller.
	AddBytes(ctx context.Context, event []byte) (string, error)
	// DeleteForDevice will delete events by the specified device name
	DeleteForDevice(ctx context.Context, deviceID string) error
	// DeleteOld deletes events according to their age
	DeleteOld(ctx context.Context, age int) error
	// Delete will delete an event by its id
	Delete(ctx context.Context, id string) error
	// MarkPushed designates an event as having been successfully exported
	MarkPushed(ctx context.Context, id string) error
	// MarkPushedByChecksum designates an event as having been successfully exported using a checksum for the respective event.
	MarkPushedByChecksum(ctx context.Context, checksum string) error
	// MarshalEvent will perform JSON or CBOR encoding of the supplied Event. If one or more Readings on the Event
	// has a populated BinaryValue, the marshaling will be CBOR. Default is JSON.
	MarshalEvent(e models.Event) ([]byte, error)
}

type eventRestClient struct {
	urlClient interfaces.URLClient
}

// NewEventClient creates an instance of EventClient
func NewEventClient(urlClient interfaces.URLClient) EventClient {
	return &eventRestClient{
		urlClient: urlClient,
	}
}

// Helper method to request and decode an event slice
func (e *eventRestClient) requestEventSlice(ctx context.Context, urlSuffix string) ([]models.Event, error) {
	data, err := clients.GetRequest(ctx, urlSuffix, e.urlClient)
	if err != nil {
		return []models.Event{}, err
	}

	eSlice := make([]models.Event, 0)
	err = json.Unmarshal(data, &eSlice)
	if err != nil {
		return []models.Event{}, err
	}

	return eSlice, nil
}

// Helper method to request and decode an event
func (e *eventRestClient) requestEvent(ctx context.Context, urlSuffix string) (models.Event, error) {
	data, err := clients.GetRequest(ctx, urlSuffix, e.urlClient)
	if err != nil {
		return models.Event{}, err
	}

	ev := models.Event{}
	err = json.Unmarshal(data, &ev)
	return ev, err
}

func (e *eventRestClient) Events(ctx context.Context) ([]models.Event, error) {
	return e.requestEventSlice(ctx, "")
}

func (e *eventRestClient) Event(ctx context.Context, id string) (models.Event, error) {
	return e.requestEvent(ctx, "/"+id)
}

func (e *eventRestClient) EventCount(ctx context.Context) (int, error) {
	return clients.CountRequest(ctx, "/count", e.urlClient)
}

func (e *eventRestClient) EventCountForDevice(ctx context.Context, deviceId string) (int, error) {
	return clients.CountRequest(ctx, "/count/"+url.QueryEscape(deviceId), e.urlClient)
}

func (e *eventRestClient) EventsForDevice(ctx context.Context, deviceID string, limit int) ([]models.Event, error) {
	return e.requestEventSlice(ctx, "/device/"+url.QueryEscape(deviceID)+"/"+strconv.Itoa(limit))
}

func (e *eventRestClient) EventsForInterval(
	ctx context.Context,
	start int,
	end int,
	limit int) ([]models.Event, error) {

	return e.requestEventSlice(ctx, "/"+strconv.Itoa(start)+"/"+strconv.Itoa(end)+"/"+strconv.Itoa(limit))
}

func (e *eventRestClient) EventsForDeviceAndValueDescriptor(
	ctx context.Context,
	deviceId string,
	vd string,
	limit int) ([]models.Event, error) {

	return e.requestEventSlice(
		ctx,
		"/device/"+
			url.QueryEscape(deviceId)+
			"/valuedescriptor/"+
			url.QueryEscape(vd)+
			"/"+strconv.Itoa(limit),
	)
}

func (e *eventRestClient) Add(ctx context.Context, event *models.Event) (string, error) {
	content := clients.FromContext(ctx, clients.ContentType)

	if content == clients.ContentTypeCBOR {
		return clients.PostRequest(ctx, "", event.CBOR(), e.urlClient)
	} else {
		return clients.PostJSONRequest(ctx, "", event, e.urlClient)
	}
}

func (e *eventRestClient) AddBytes(ctx context.Context, event []byte) (string, error) {
	return clients.PostRequest(ctx, "", event, e.urlClient)
}

func (e *eventRestClient) Delete(ctx context.Context, id string) error {
	return clients.DeleteRequest(ctx, "/id/"+id, e.urlClient)
}

func (e *eventRestClient) DeleteForDevice(ctx context.Context, deviceID string) error {
	return clients.DeleteRequest(ctx, "/device/"+url.QueryEscape(deviceID), e.urlClient)
}

func (e *eventRestClient) DeleteOld(ctx context.Context, age int) error {
	return clients.DeleteRequest(ctx, "/removeold/age/"+strconv.Itoa(age), e.urlClient)
}

func (e *eventRestClient) MarkPushed(ctx context.Context, id string) error {
	_, err := clients.PutRequest(ctx, "/id/"+id, nil, e.urlClient)
	return err
}

func (e *eventRestClient) MarkPushedByChecksum(ctx context.Context, checksum string) error {
	_, err := clients.PutRequest(ctx, "/checksum/"+checksum, nil, e.urlClient)
	return err
}

func (e *eventRestClient) MarshalEvent(event models.Event) (data []byte, err error) {
	for _, r := range event.Readings {
		if len(r.BinaryValue) > 0 {
			return event.CBOR(), nil
		}
	}
	return json.Marshal(event)
}
